from Commons import *
from Creature import *
from Item import *
map_console = L.console_new(MAX_MAP_WIDTH,MAX_MAP_HEIGHT)#the console where the map is displayed
fov_map = L.map_new(MAX_MAP_WIDTH,MAX_MAP_HEIGHT)
#flags
scrolling = "Scrolling"
no_fog_of_war = "No Fog of War"
#Tile structure: Tile contains foreground, background, character, blocks light and blocks movement
#dark tile, light tile, (blocked, see-through)
#layers and layer consoles, hold information on 
layers = [L_TRA, L_INV, L_RAN, L_CRE]
def AStarPathfinding(fromx, fromy, tox, toy):#return the A star path for x
    path = L.path_new_using_map(fov_map,1)
    L.path_compute(path, fromx, fromy, tox, toy)#compute the path
    return path#return it

#allows the player to select a viewed space with pathfinding
def AStarPlayerTravelPathFinding(fromx, fromy, tox, toy):
    path = L.path_new_using_function()

def switchMap(newmap):#switch the map
    Game()[A_MAP] = newmap
    #add the player
    Game()[A_MAP][L_CRE].addObject(Game()[A_PLAYER],Game()[A_PLAYER].x,Game()[A_PLAYER].y,'@',L.red,False)

class Tile():#wrapper for tile class
    def __init__(self,unlit_tile=(L.black,L.black,' '),
                 lit_tile=(L.black,L.black,' '),
                 block_light=True,block_movement=True):
        self.unlit_tile = unlit_tile
        self.lit_tile = lit_tile
        self.block_light = block_light
        self.block_movement = block_movement
        self.lit = False#is the tile lit?
        self.has_seen = False#if false, the tile is always seen, may be useful later ;)
#universal tile definitions -- cannot be modified
global universaltiledefs
universaltiledefs = {"#":((L.black,color_dark_wall,' '),
                     (L.black,color_light_wall,' '),
                     True,True),
            ".":((L.black,color_dark_ground,' '),
            
                     (L.black,color_light_ground,' '),
                     False,False)}

class Layer():#layers hold objects for easy map retrieval and can display them
    def __init__(self,is_trap_layer=False):#is_trap_layer means that colliding with something triggers an effect
        self.console = L.console_new(MAX_MAP_WIDTH,MAX_MAP_HEIGHT)
        self.objects = []#objects that the layer contains
        self.is_trap_layer = is_trap_layer
        L.console_set_key_color(self.console,L.black)#blit only the parts in los
    def __iter__(self):#iterator test
        return [a[0] for a in self.objects]
    def addObject(self,object,xpos,ypos,char,fore):#adds a layer object
        L.console_set_fore(self.console,xpos,ypos,fore)
        L.console_set_char(self.console,xpos,ypos,char)
        self.objects.append([object,xpos,ypos,char,fore])
        #if (Game()[A_MAP].flags.__contains__(scrolling)):#allow space for scrolling
        self.modifyObjectDisplay(object,xpos,ypos,fore,char)
        
    def __getitem__(self,key):
        if (not isinstance(key,tuple) or len(key) != 2):#raise an exception if not proper format
            raise KeyError
        for item in self.objects:#check all of the objects
            if (item[1] == key[0] and item[2] == key[1]):
                return item[0]
        return None#if nothing matches
    def modifyObjectDisplay(self,theobject,xpos=None,ypos=None,fore=None,char=None):#modify what it looks like
        for item in self.objects:
            if (item[0] == theobject):
                if (xpos != None or ypos != None):#change in the x and y position
                    L.console_set_char(self.console,item[1],item[2],' ')#remove the old character
                    if (xpos != None):#change the stored positions
                        item[1] = xpos
                    if (ypos != None):
                        item[2] = ypos
                if (char != None):
                    item[3] = char
                if (fore != None):
                    item[4] = fore
                #change the character and foreground of the new position
                L.console_set_char(self.console,item[1],item[2],item[3])
                L.console_set_fore(self.console,item[1],item[2],item[4])
    def destroyObject(self,theobject):#destroy the object
        for item in self.objects:#check all of the objects
            if (item[0] == theobject):#match, so remove
                L.console_set_char(self.console,item[1],item[2],' ')
                self.objects.remove(item)
                return
    def destroy(self):#call before deleting the layer!
        L.console_delete(self.console)
        del self
    def save(self,FILE):
        cPickle.dump(len(self.objects),FILE)#dump the length of the array
        for OBJECT in self.objects:#save the objects
            cPickle.dump(OBJECT,FILE)
    def load(self,FILE):
        arraylength = cPickle.load(FILE)#load the length of the array
        for i in range(arraylength):
            self.addObject(*tuple(cPickle.load(FILE)))
class ScheduledLayer(Layer):#a scheduled layer
    def __init__(self,is_trap_layer):
        Layer.__init__(self,is_trap_layer)
        self.schedule = {}
        self.time = 0L
    def addObject(self,object,xpos,ypos,char,fore,isSave=True):
        #copied from addObject
        L.console_set_fore(self.console,xpos,ypos,fore)
        L.console_set_char(self.console,xpos,ypos,char)
        self.objects.append([object,xpos,ypos,char,fore])
        #if (Game()[A_MAP].flags.__contains__(scrolling)):#allow space for scrolling
        self.modifyObjectDisplay(object,xpos,ypos,fore,char)
        if (isSave):#true if restoring from savegame
            self.addEvent(object,object.nextTime-self.time)#do a +1 so that it records it instead of doing a timestop
        else:
            self.addEvent(object,10)#10 turn inital delay XXX: change?
    def addEvent(self,object,delay):#add an event to the schedule
        if (delay < -1):
            print "Delay cannot be less than -1!"
            raise Exception
        if (delay == -1):#delay must be more than -1, -1 indicates immediate additional turn
            return True#take another turn instead
        #tell the creature when it should act next for saving purposes
        object.nextTime = self.time+delay
        if (self.schedule.__contains__(self.time+delay)):
            self.schedule[self.time+delay].append(object)
        else:
            self.schedule[self.time+delay] = [object]
        return False#no turn for you
    def tick(self):#have things take turns and add time
        if (self.schedule.__contains__(self.time)):#if it contains time
            while (len(self.schedule[self.time]) > 0):
                anobject = self.schedule[self.time][0]#the object
                takeaturn = True
                while (takeaturn):#while taking a turn
                    foundobject = False
                    for OBJECT in self.objects:#does it exist
                        if (not foundobject and OBJECT[0] == anobject):
                            delay = anobject.act()
                            takeaturn = self.addEvent(anobject,delay)
                            foundobject = True
                    if (not foundobject):#the object has not been found
                        takeaturn = False#it doesn't exist, don't take a turn
                        
                self.schedule[self.time].pop(0)#get rid of the first one to move to the next
            self.schedule.pop(self.time)#remove the time from the schedule
        self.time+=1
    def save(self, FILE):
        
        cPickle.dump(self.time,FILE)
        #save the layer
        cPickle.dump(len(self.objects),FILE)#dump the length of the array
        for OBJECT in self.objects:#save the objects
            cPickle.dump(OBJECT,FILE)

    def load(self, FILE):
        self.time = cPickle.load(FILE)
        arraylength = cPickle.load(FILE)#load the length of the array
        for i in range(arraylength):
            self.addObject(*tuple(cPickle.load(FILE)))

class CreatureLayer(ScheduledLayer):#A scheduled layer with a special call for the player
    def __init__(self):
        ScheduledLayer.__init__(self,False)
    def getPlayer(self):
        for OBJECT in self.objects:#for each object
            if (OBJECT[0].isPlayer):#if the first item (the creature) is the player
                return OBJECT[0]#return the player
        return None#nothing found

class Map:
    #variables to define in the program
    map = ["###############",
           "#.............#",
           "###############"]
    flags=()
    tiles=[]
    specialdefs = {}#special definitions for tiles
    maplayers = [Layer(True), Layer(False), ScheduledLayer(True), CreatureLayer()]#the map layers, displayed from bottom to top
    #layers = 
    #Variables handle scrolling
    scrollx = 0
    scrolly = 0
    def tick(self):#tick all the layers, if possible
        for layer in self.maplayers:
            try:
                layer.tick()
            except AttributeError:#can't be ticked
                pass
    #last fov position, makes recalculating fov easier
    def initialize(self):
        pass
    def __getitem__(self,key):
        if (key in layers):#get the layer
            return self.maplayers[layers.index(key)]
        #XXX:add check for tuple in getitem and setitem
        if (isinstance(key,tuple) and len(key) == 2):#only other valid format is 2 arg tuple
            if (key[0] >= len(self.tiles) or key[0] < 0 or key[1] >= len(self.tiles[0]) or key[1] < 0):
                raise IndexError
            return self.tiles[key[0]][key[1]]#return a part of the map
        raise KeyError
    def __setitem__(self,key,value):
        if (key[0] >= len(self.tiles) or key[0] < 0 or key[1] >= len(self.tiles[0]) or key[1] < 0):
            raise IndexError
        if (not isinstance(key,tuple)):#raise an error if it isn't a tuple
            raise KeyError
        self.tiles[key[0]][key[1]] = value
        self.updateTile(key[0],key[1])#update the tile when it is modified
        if (self.tiles[key[0]][key[1]].lit):
            self.updateTile(key[0],key[1])
    def save(self,FILE):#the tiles need to be specially saved
        cPickle.dump(self.tiles,FILE)
        cPickle.dump(len(self.maplayers),FILE)
        for maplayer in self.maplayers:
            maplayer.save(FILE)
    def load(self,FILE):
        self.tiles = cPickle.load(FILE)
        maplayerlength = cPickle.load(FILE)
        for i in range(maplayerlength):
            self.maplayers[i].load(FILE)
    def readMap(self):#reads a map--used to initialize the map
        #create the blank tile array (we are transposing it from the given map
        self.initialize()#initialize the map
        self.scrollx = Game()[A_PLAYER].x-7
        self.scrolly = Game()[A_PLAYER].y-7
        for x in range(len(self.map[0])):
            self.tiles.append([None for y in range(len(self.map))])
        for x in range(len(self.tiles)):
            for y in range(len(self.tiles[x])):
                try:
                    self.tiles[x][y]=Tile(*universaltiledefs[self.map[y][x]])
                    if (self.flags.__contains__(no_fog_of_war) or self.tiles[x][y].has_seen):#update everything if there is no fog of war
                        self.renderTile(x,y)
                except KeyError:
                    try:
                        self.tiles[x][y]=Tile(*self.specialdefs[self.map[y][x]])
                        if (self.flags.__contains__(no_fog_of_war) or self.tiles[x][y].has_seen):#update everything if there is no fog of war
                            self.renderTile(x,y)
                    except KeyError:
                        print "ERROR Could not find tile"
                self.updateTile(x,y)
        try:#test
            self.aftermapscript()
        except Exception:
            traceback.print_exc()
        self.recalculateFOV()
    def restoreMap(self):#used to restore a map that has been saved
        for x in range(len(self.tiles)):
            for y in range(len(self.tiles[x])):
                self.updateTile(x,y)
                if (self.flags.__contains__(no_fog_of_war) or self.tiles[x][y].has_seen):#update everything if there is no fog of war
                    self.renderTile(x,y)
        #include a check for if the player's FOV should be updated
        fov_darken = Game()[A_PLAYER].displaytype == S_MAP
        self.recalculateFOV(fov_darken)
    def renderTile(self,xpos,ypos):#render a tile (based on the map)
        if (self[(xpos,ypos)].lit or self.flags.__contains__(no_fog_of_war)):#lit
            tiledata = self[(xpos,ypos)].lit_tile
            for layer in self.maplayers:
                L.console_set_back(layer.console,xpos,ypos,L.red,L.BKGND_SET)#tile is visible for los_only
        else:
            #do what was previously seen here for the map console
            tiledata = (self[(xpos,ypos)].unlit_tile[0], self[(xpos,ypos)].unlit_tile[1],
                         L.console_get_char(map_console,xpos,ypos))
            for layer in self.maplayers:
                L.console_set_back(layer.console,xpos,ypos,L.black,L.BKGND_SET)#tile is invisible for los_only
                #do what was previously seen here -- override previous stuff
                if (L.console_get_char(layer.console,xpos,ypos) != 32):
                    #XXX: should our replaced foreground be constant?
                    tiledata = (L.grey, tiledata[1], L.console_get_char(layer.console,xpos,ypos))
        L.console_put_char_ex(map_console,xpos,ypos,tiledata[2],tiledata[0],tiledata[1])
    def updateTile(self,xpos,ypos):#updates the map if a tile changes
        L.map_set_properties(fov_map,xpos,ypos,not self[(xpos, ypos)].block_movement, not self[(xpos, ypos)].block_light)
    def blitRectangle(self,x0,y0,w,h,x1,y1):
        L.console_blit(map_console,x0,y0,w,h,None,x1,y1)
        for layer in self.maplayers:
            L.console_blit(layer.console,x0,y0,w,h,None,x1+VIEWING_AREA_SCROLLX,y1+VIEWING_AREA_SCROLLY,1.0,0.0)
    #recalculate the fov and update the map
    #if everything_unseen is True, update it as though not
    def recalculateFOV(self,everything_unseen=False):
        px = Game()[A_PLAYER].x
        py = Game()[A_PLAYER].y
        #do the calculation
        L.map_compute_fov(fov_map,px,py, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
        for x in range(len(self.tiles)):
            for y in range(len(self.tiles[0])):
                beforelight = self.tiles[x][y].lit
                if (L.map_is_in_fov(fov_map,x,y) and not everything_unseen):
                    self.tiles[x][y].lit = True
                    self.tiles[x][y].has_seen = True
                else:
                    self.tiles[x][y].lit = False
                if (beforelight != self.tiles[x][y].lit):
                    self.renderTile(x,y)
    def addCreature(self,object):#adds a creature
        self[L_CRE].addObject(object,object.x,object.y,object.letter,object.color,False)
    def addItem(self,x,y,object):#adds an item to a particular place on the map
        inventory = self[L_INV][(x,y)]
        if (inventory != None):#add the item to an existing inventory
            inventory.addItem(object)
            self[L_INV].modifyObjectDisplay(inventory,fore=inventory.color,char=inventory.character)
        else:#add an entirely new inventory
            self[L_INV].addObject(GroundInventory(object),x,y,object.character,object.color)
    #checks for traps after creature movement
    def trapEffects(self,creature):
        traps = []
        #get all of the traps
        for layer in self.maplayers:
            if (layer.is_trap_layer):
                for anobject in layer.objects:
                    if (anobject[1] == creature.x and anobject[2] == creature.y):
                        traps.append(anobject[0])
        #spring them
        for trap in traps:
            trap.collisionEffect(creature)
            
    
    #removes an item from a particular place on the map and returns the removed object
    def removeItem(self,x,y,object):
        if (self[L_INV][(x,y)] == None):
            raise IndexError#wrong place
        inv = self[L_INV][(x,y)]
        try:
            inventorydone = inv.removeItem(object)
        except KeyError:#error
            print "Error, removeItem could not remove object "+ str(object) + " from the map!"
            return None
        if (inventorydone):#done
            self[L_INV].destroyObject(inv)
        else:#not, modify the display
            self[L_INV].modifyObjectDisplay(inv,fore=inv.color,char=inv.character)
    def showMap(self,width = VIEWING_AREA_WIDTH, height = VIEWING_AREA_HEIGHT):#use libtcod to show the map
        if (self.flags.__contains__(scrolling)):#if scrolling
            #handle boundries
            x0=self.scrollx
            y0=self.scrolly
            x1=0
            y1=0
            w=width
            h=height
            if (self.scrollx < 0):#subtract rectangles
                x0 = 0
                x1 = 0-self.scrollx
                w += self.scrollx
            if (self.scrollx > MAX_MAP_WIDTH-VIEWING_AREA_WIDTH):
                w -= self.scrollx-MAX_MAP_WIDTH+VIEWING_AREA_WIDTH
            if (self.scrolly < 0):#subtract rectangles
                y0 = 0
                y1 = 0-self.scrolly
                h += self.scrolly
            if (self.scrolly > MAX_MAP_HEIGHT-VIEWING_AREA_HEIGHT):
                h -= self.scrolly-MAX_MAP_HEIGHT+VIEWING_AREA_HEIGHT    
            self.blitRectangle(x0,y0,w,h,x1,y1)
        else:#no scrolling
            self.blitRectangle(0,0,15,15,0,0)
